﻿/*******************************************************************************
 * Copyright (c) 2007
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Moscow State University nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.IO;

namespace Faml
{
    public class OperationLibrary
    {
        private Dictionary<string, OperationContainer> library;
        private OperationInstanceProvider instanceProvider;

        public OperationLibrary(OperationInstanceProvider instanceProvider)
        {
            library = new Dictionary<string, OperationContainer>();
            this.instanceProvider = instanceProvider;
        }

        private OperationLibrary(Dictionary<string, OperationContainer> library, OperationInstanceProvider instanceProvider)
        {
            this.library = library;
            this.instanceProvider = instanceProvider;
        }

        public OperationInstanceProvider OperationInstanceProvider
        {
            set { 
                this.instanceProvider = value;
                foreach (OperationContainer container in library.Values)
                {
                    container.OperationLibrary = this;
                    foreach (Operation op in container.Operations)
                    {
                        op.InstanceObject = null;
                    }
                }
            }
            get { return this.instanceProvider; }
        }

        public int GetOperationsNumber()
        {
            return library.Keys.Count;
        }

        public static OperationLibrary CreateFrom(OperationLibrary opLib)
        {
            return new OperationLibrary(opLib.library, opLib.instanceProvider);
        }

        public string GetStatistics()
        {
            StringBuilder sb = new StringBuilder();

            sb.AppendFormat("Operations ({0}): ", library.Count);
            foreach (string opname in library.Keys)
            {
                sb.Append(opname);
                sb.Append(", ");
            }

            return sb.ToString();
        }

        public Operation GetOperation(string operation, Type genericType, Type[] argTypes)
        {
            OperationContainer container;
            if (!library.TryGetValue(operation.ToLower(), out container))
                throw new ApplicationException("Operation " + operation + " not found");

            return container.GetOperation(new Type[] { genericType }, argTypes);
        }

        public Operation GetOperation(string operation, Type[] genericTypes, Type[] argTypes)
        {
            OperationContainer container;
            if (!library.TryGetValue(operation.ToLower(), out container))
                throw new ApplicationException("Operation " + operation + " not found");

            return container.GetOperation(genericTypes, argTypes);
        }

        public Operation GetOperation(string operation, Type[] argTypes)
        {
            OperationContainer container;
            if (!library.TryGetValue(operation.ToLower(), out container))
                throw new ApplicationException("Operation " + operation + " not found");

            return container.GetOperation(argTypes);
        }

        public void AddMethods(string assemblyName)
        {
            Assembly a;

            try
            {
                a = Assembly.LoadFrom(assemblyName);
            }
            catch (FileNotFoundException)
            {
                a = Assembly.LoadFrom(Path.Combine("External Libraries", assemblyName));
            }

            AddMethods(a);
        }

        public int AddMethods(Assembly assembly)
        {
            Type[] types = assembly.GetExportedTypes();

            int n = library.Keys.Count;
            for (int i = 0; i < types.Length; i++)
            {
                Type t = types[i];
                if (!t.IsClass) continue;

                try
                {
                    AddMethods(t);
                }
                catch (Exception ex)
                {
                    //Debug.WriteLine(ex.Message);
                }
            }

            return library.Keys.Count - n;
        }

        public int AddMethods(Type operationContainer)
        {
            if(!operationContainer.IsClass)
                throw new ApplicationException("Class must be a class to be added to operation library");

            if (operationContainer.GetCustomAttributes(typeof(OperationContainerAttribute), false).Length == 0)
                throw new ApplicationException("Class must have an OperationContainer attribute to be added to operation library");

            MethodInfo[] methods = operationContainer.GetMethods();
            int n = library.Keys.Count;
            for (int i = 0; i < methods.Length; i++)
            {
                try
                {
                    AddMethod(methods[i]);                    
                }
                catch (Exception ex)
                {
                    //Debug.WriteLine(ex.Message);
                }
            }

            return library.Keys.Count - n;
        }

        public void AddMethod(MethodInfo operationMethod)
        {
            object[] attrs;
            if ((attrs = operationMethod.GetCustomAttributes(typeof(OperationNameAttribute), false)).Length == 0) 
                throw new ApplicationException("Method must have an OperationName attribute to be added to operation library");
            /*if (!operationMethod.IsStatic)
                throw new ApplicationException("Method must be static to be added to operation library");*/

            OperationNameAttribute aName = attrs[0] as OperationNameAttribute;
            string opname = aName.Name.ToLower();

            OperationContainer container;
            if (!library.TryGetValue(opname, out container))
            {
                container = new OperationContainer(opname, this);
                library.Add(container.Name, container);
            }

            container.AddImplementation(operationMethod);
        }


        /// <summary>
        /// OperationContainer class collects operations with the same name.
        /// </summary>
        private class OperationContainer
        {
            private string name;
            private List<Operation> operations;
            private OperationLibrary library;

            public OperationContainer(string name, OperationLibrary library)
            {
                this.name = name;
                this.operations = new List<Operation>();
                this.library = library;
            }

            internal OperationLibrary OperationLibrary
            {
                get { return library; }
                set { library = value; }
            }

            internal List<Operation> Operations
            {
                get { return operations; }
            }

            public string Name
            {
                get { return name; }
            }

            public void AddImplementation(MethodInfo method)
            {
                Operation op = new Operation(name, method);
                operations.Add(op);
            }

            private bool MatchParameter(Type parType, Type argType, Type[] genArgs, Type[] genValues)
            {
                if (parType == argType)
                    return true;
                if (parType.Name == argType.Name && parType.ContainsGenericParameters)
                {
                    Type[] argGenArgs = argType.GetGenericArguments();
                    Type[] parGenArgs = parType.GetGenericArguments();
                    if (argGenArgs.Length == parGenArgs.Length)
                        for (int i = 0; i < argGenArgs.Length; i++)
                        {
                            int genIdx = -1;
                            for (int j = 0; j < genArgs.Length; j++)
                                if (genArgs[j] == parGenArgs[i])
                                {
                                    genIdx = j;
                                    break;
                                }
                            if (genIdx != -1)
                            {
                                if (genValues[genIdx] == null)
                                    genValues[genIdx] = argGenArgs[i];
                                else if (genValues[genIdx] != argGenArgs[i])
                                    return false;
                            }
                            if (!MatchParameter(parGenArgs[i], argGenArgs[i], genArgs, genValues))
                                return false;
                        }
                    return true;
                }
                else if (parType.IsArray && argType.IsArray && parType.GetArrayRank() == argType.GetArrayRank())
                    return MatchParameter(parType.GetElementType(), argType.GetElementType(), genArgs, genValues);
                else
                {
                    int genIdx = -1;
                    for (int j = 0; j < genArgs.Length; j++)
                        if (genArgs[j] == parType)
                        {
                            genIdx = j;
                            break;
                        }
                    if (genIdx != -1)
                    {
                        if (genValues[genIdx] == null)
                        {
                            genValues[genIdx] = argType;
                            return true;
                        }
                        else return (genValues[genIdx] == argType);
                    }
                }
                return false;
            }

            public Operation GetOperation(Type[] argTypes)
            {
                foreach (Operation op in operations)
                {
                    MethodInfo method = op.Method;
                    MethodInfo imethod = null;
                    if (method.IsGenericMethod)
                    {
                        TypeMatcher tm = new TypeMatcher(method.GetGenericArguments());
                        if (tm.Match(method.GetParameters(), argTypes))
                            imethod = method.MakeGenericMethod(tm.ActualTypes);
                    }
                    else
                    {
                        imethod = method;
                    }

                    if (imethod != null)
                    {
                        ParameterInfo[] parameters2 = imethod.GetParameters();
                        if (parameters2.Length != argTypes.Length) continue;

                        bool found = true;
                        for (int i = 0; (i < parameters2.Length) && found; i++)
                        {
                            if (parameters2[i].ParameterType != argTypes[i])
                                found = false;
                        }

                        if (found)
                            return PrepareOperation(op, imethod);
                    }
                }

                throw new ApplicationException("An operation (" + this.name + ") doesn't support requested parameters types");
            }

            public Operation GetOperation(Type[] genericTypes, Type[] argTypes)
            {
                foreach (Operation operation in operations)
                {
                    MethodInfo method = operation.Method;

                    try
                    {
                        MethodInfo imethod;
                        if (method.IsGenericMethod)
                        {
                            imethod = method.MakeGenericMethod(genericTypes);
                        }
                        else if (method.ContainsGenericParameters)
                        {
                            Type t = method.DeclaringType;
                            t = t.MakeGenericType(genericTypes);

                            imethod = t.GetMethod(method.Name, BindingFlags.Public | BindingFlags.Static,
                                null, argTypes, null);
                            if (imethod != null)
                                return PrepareOperation(operation, imethod);
                            continue;
                        }
                        else
                        {
                            imethod = method;
                        }

                        ParameterInfo[] parameters = imethod.GetParameters();
                        if (parameters.Length != argTypes.Length) continue;

                        bool found = true;
                        for (int i = 0; (i < parameters.Length) && found; i++)
                        {
                            if (parameters[i].ParameterType != argTypes[i])
                                found = false;
                        }

                        if (found)
                        {
                            return PrepareOperation(operation, imethod);
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }

                throw new ApplicationException("An operation (" + this.name + ") doesn't support requested parameters types");
            }

            private Operation PrepareOperation(Operation baseOp, MethodInfo implMethod)
            {
                if (implMethod.IsStatic)
                    return new Operation(name, implMethod);

                //===========================================
                // Instance method:
                //  we have to get an instance for it
                Debug.Assert(baseOp.Method.DeclaringType.IsGenericType == false,
                        string.Format("Non static method {1} is used in a generic type {0}; it is not supported by OperationLibrary",
                            baseOp.Method.DeclaringType, baseOp.Method));

                object instance = (baseOp.InstanceObject == null) ?
                    baseOp.InstanceObject = library.OperationInstanceProvider(baseOp.Method) :
                    baseOp.InstanceObject;
                return new Operation(name, implMethod, instance);
            }
        }
    }

    public delegate object OperationInstanceProvider(MethodInfo method);

}
